/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.context.embedded;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.ServletContextInitializerBeans;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.Resource;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.context.support.ServletContextAwareProcessor;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.util.Collection;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

/**
 * A {@link WebApplicationContext} that can be used to bootstrap itself from a contained
 * {@link EmbeddedServletContainerFactory} bean.
 * <p>
 * This context will create, initialize and run an {@link EmbeddedServletContainer} by
 * searching for a single {@link EmbeddedServletContainerFactory} bean within the
 * {@link ApplicationContext} itself. The {@link EmbeddedServletContainerFactory} is free
 * to use standard Spring concepts (such as dependency injection, lifecycle callbacks and
 * property placeholder variables).
 * <p>
 * In addition, any {@link Servlet} or {@link Filter} beans defined in the context will be
 * automatically registered with the embedded Servlet container. In the case of a single
 * Servlet bean, the '/' mapping will be used. If multiple Servlet beans are found then
 * the lowercase bean name will be used as a mapping prefix. Any Servlet named
 * 'dispatcherServlet' will always be mapped to '/'. Filter beans will be mapped to all
 * URLs ('/*').
 * <p>
 * For more advanced configuration, the context can instead define beans that implement
 * the {@link ServletContextInitializer} interface (most often
 * {@link ServletRegistrationBean}s and/or {@link FilterRegistrationBean}s). To prevent
 * double registration, the use of {@link ServletContextInitializer} beans will disable
 * automatic Servlet and Filter bean registration.
 * <p>
 * Although this context can be used directly, most developers should consider using the
 * {@link AnnotationConfigEmbeddedWebApplicationContext} or
 * {@link XmlEmbeddedWebApplicationContext} variants.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @see AnnotationConfigEmbeddedWebApplicationContext
 * @see XmlEmbeddedWebApplicationContext
 * @see EmbeddedServletContainerFactory
 */
public class EmbeddedWebApplicationContext extends GenericWebApplicationContext
{

    private static final Log logger = LogFactory.getLog(EmbeddedWebApplicationContext.class);

    /**
     * Constant value for the DispatcherServlet bean name. A Servlet bean with this name
     * is deemed to be the "main" servlet and is automatically given a mapping of "/" by
     * default. To change the default behaviour you can use a
     * {@link ServletRegistrationBean} or a different bean name.
     */
    public static final String DISPATCHER_SERVLET_NAME = "dispatcherServlet";

    private volatile EmbeddedServletContainer embeddedServletContainer;

    private ServletConfig servletConfig;

    private String namespace;

    /**
     * Register ServletContextAwareProcessor.
     *
     * @see ServletContextAwareProcessor
     */
    @Override
    protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
    {
        beanFactory.addBeanPostProcessor(new WebApplicationContextServletContextAwareProcessor(this));
        beanFactory.ignoreDependencyInterface(ServletContextAware.class);
    }

    @Override
    public final void refresh() throws BeansException, IllegalStateException
    {
        try
        {
            super.refresh();
        }
        catch (RuntimeException ex)
        {
            stopAndReleaseEmbeddedServletContainer();
            throw ex;
        }
    }

    @Override
    protected void onRefresh()
    {
        super.onRefresh();
        try
        {
            createEmbeddedServletContainer();
        }
        catch (Throwable ex)
        {
            throw new ApplicationContextException("Unable to start embedded container", ex);
        }
    }

    @Override
    protected void finishRefresh()
    {
        super.finishRefresh();
        EmbeddedServletContainer localContainer = startEmbeddedServletContainer();
        if (localContainer != null)
        {
            publishEvent(new EmbeddedServletContainerInitializedEvent(this, localContainer));
        }
    }

    @Override
    protected void onClose()
    {
        super.onClose();
        stopAndReleaseEmbeddedServletContainer();
    }

    /**
     * 创建 tomcat jetty 等容器
     */
    private void createEmbeddedServletContainer()
    {
        EmbeddedServletContainer localContainer = this.embeddedServletContainer;
        ServletContext localServletContext = getServletContext();
        if (localContainer == null && localServletContext == null)
        {
            // 如果是 tomcat 则获取 TomcatEmbeddedServletContainerFactory
            EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
            // 这里会去实例化一个 Tomcat 并且配置好相应的配置信息，配置 默认 host，日志文件，initializers等，然后会把 该容器启动
            this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());
        }
        // DispatcherServlet 是由 DispatcherServletAutoConfiguration 来启动
        // 从注解@AutoCondifureAfter可以看出，该类是在 EmbeddedServletContainerAutoConfiguration 之后初始化。
        else if (localServletContext != null)
        {
            try
            {
                getSelfInitializer().onStartup(localServletContext);
            }
            catch (ServletException ex)
            {
                throw new ApplicationContextException("Cannot initialize servlet context", ex);
            }
        }
        initPropertySources();
    }

    /**
     * Returns the {@link EmbeddedServletContainerFactory} that should be used to create
     * the embedded servlet container. By default this method searches for a suitable bean
     * in the context itself.
     *
     * @return a {@link EmbeddedServletContainerFactory} (never {@code null})
     */
    protected EmbeddedServletContainerFactory getEmbeddedServletContainerFactory()
    {
        // Use bean names so that we don't consider the hierarchy
        // org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration.EmbeddedTomcat.tomcatEmbeddedServletContainerFactory
        String[] beanNames = getBeanFactory().getBeanNamesForType(EmbeddedServletContainerFactory.class);
        if (beanNames.length == 0)
        {
            throw new ApplicationContextException("Unable to start EmbeddedWebApplicationContext due to missing ");
        }
        if (beanNames.length > 1)
        {
            throw new ApplicationContextException("Unable to start EmbeddedWebApplicationContext due to multiple ");
        }
        return getBeanFactory().getBean(beanNames[0], EmbeddedServletContainerFactory.class);
    }

    /**
     * Returns the {@link ServletContextInitializer} that will be used to complete the
     * setup of this {@link WebApplicationContext}.
     *
     * @return the self initializer
     * @see #prepareEmbeddedWebApplicationContext(ServletContext)
     */
    private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer()
    {
        return new ServletContextInitializer()
        {
            @Override
            public void onStartup(ServletContext servletContext) throws ServletException
            {
                selfInitialize(servletContext);
            }
        };
    }

    private void selfInitialize(ServletContext servletContext) throws ServletException
    {
        prepareEmbeddedWebApplicationContext(servletContext);
        ConfigurableListableBeanFactory beanFactory = getBeanFactory();
        ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(beanFactory);
        WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, getServletContext());
        existingScopes.restore();
        WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, getServletContext());
        for (ServletContextInitializer beans : getServletContextInitializerBeans())
        {
            beans.onStartup(servletContext);
        }
    }

    /**
     * Returns {@link ServletContextInitializer}s that should be used with the embedded
     * Servlet context. By default this method will first attempt to find
     * {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain
     * {@link EventListener} beans.
     *
     * @return the servlet initializer beans
     */
    protected Collection<ServletContextInitializer> getServletContextInitializerBeans()
    {
        return new ServletContextInitializerBeans(getBeanFactory());
    }

    /**
     * Prepare the {@link WebApplicationContext} with the given fully loaded
     * {@link ServletContext}. This method is usually called from
     * {@link ServletContextInitializer#onStartup(ServletContext)} and is similar to the
     * functionality usually provided by a {@link ContextLoaderListener}.
     *
     * @param servletContext the operational servlet context
     */
    protected void prepareEmbeddedWebApplicationContext(ServletContext servletContext)
    {
        Object rootContext = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
        if (rootContext != null)
        {
            if (rootContext == this)
            {
                throw new IllegalStateException(
                        "Cannot initialize context because there is already a root application context present - " +
                                "check whether you have multiple ServletContextInitializers!");
            }
            return;
        }
        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring embedded WebApplicationContext");
        try
        {
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);
            if (logger.isDebugEnabled())
            {
                logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
                                     WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
            }
            setServletContext(servletContext);
            if (logger.isInfoEnabled())
            {
                long elapsedTime = System.currentTimeMillis() - getStartupDate();
                logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
            }
        }
        catch (RuntimeException ex)
        {
            logger.error("Context initialization failed", ex);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
            throw ex;
        }
        catch (Error ex)
        {
            logger.error("Context initialization failed", ex);
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
            throw ex;
        }
    }

    private EmbeddedServletContainer startEmbeddedServletContainer()
    {
        EmbeddedServletContainer localContainer = this.embeddedServletContainer;
        if (localContainer != null)
        {
            localContainer.start();
        }
        return localContainer;
    }

    private void stopAndReleaseEmbeddedServletContainer()
    {
        EmbeddedServletContainer localContainer = this.embeddedServletContainer;
        if (localContainer != null)
        {
            try
            {
                localContainer.stop();
                this.embeddedServletContainer = null;
            }
            catch (Exception ex)
            {
                throw new IllegalStateException(ex);
            }
        }
    }

    @Override
    protected Resource getResourceByPath(String path)
    {
        if (getServletContext() == null)
        {
            return new ClassPathContextResource(path, getClassLoader());
        }
        return new ServletContextResource(getServletContext(), path);
    }

    @Override
    public void setNamespace(String namespace)
    {
        this.namespace = namespace;
    }

    @Override
    public String getNamespace()
    {
        return this.namespace;
    }

    @Override
    public void setServletConfig(ServletConfig servletConfig)
    {
        this.servletConfig = servletConfig;
    }

    @Override
    public ServletConfig getServletConfig()
    {
        return this.servletConfig;
    }

    /**
     * Returns the {@link EmbeddedServletContainer} that was created by the context or
     * {@code null} if the container has not yet been created.
     *
     * @return the embedded servlet container
     */
    public EmbeddedServletContainer getEmbeddedServletContainer()
    {
        return this.embeddedServletContainer;
    }

    /**
     * Utility class to store and restore any user defined scopes. This allow scopes to be
     * registered in an ApplicationContextInitializer in the same way as they would in a
     * classic non-embedded web application context.
     */
    public static class ExistingWebApplicationScopes
    {

        private static final Set<String> SCOPES;

        static
        {
            Set<String> scopes = new LinkedHashSet<String>();
            scopes.add(WebApplicationContext.SCOPE_REQUEST);
            scopes.add(WebApplicationContext.SCOPE_SESSION);
            scopes.add(WebApplicationContext.SCOPE_GLOBAL_SESSION);
            SCOPES = Collections.unmodifiableSet(scopes);
        }

        private final ConfigurableListableBeanFactory beanFactory;

        private final Map<String, Scope> scopes = new HashMap<String, Scope>();

        public ExistingWebApplicationScopes(ConfigurableListableBeanFactory beanFactory)
        {
            this.beanFactory = beanFactory;
            for (String scopeName : SCOPES)
            {
                Scope scope = beanFactory.getRegisteredScope(scopeName);
                if (scope != null)
                {
                    this.scopes.put(scopeName, scope);
                }
            }
        }

        public void restore()
        {
            for (Map.Entry<String, Scope> entry : this.scopes.entrySet())
            {
                if (logger.isInfoEnabled())
                {
                    logger.info("Restoring user defined scope " + entry.getKey());
                }
                this.beanFactory.registerScope(entry.getKey(), entry.getValue());
            }
        }

    }

}
